#include "xen/renderer/renderer3d.hpp"
#include "xen/core.hpp"

namespace Xen {

struct Renderer3DStorage {
    Ref<Shader> shader;
    Ref<Texture2D> white_texture;
    Ref<Texture2D> black_texture;
    Ref<Texture2D> blue_texture;    // for normal mapping

    DirectLight dirlight;
    DotLight dotlight;
    SpotLight spotlight;
    glm::vec3 view_pos;

    glm::mat4 view;
    glm::mat4 projection;

    Ref<Shader> skybox_shader;
    Ref<CubeMap> skybox = nullptr;
    Ref<VertexArray> skybox_vao = nullptr;
};

static Renderer3DStorage data;

Ref<CubeMap> Renderer3D::CreateSkyBox(const std::string& top, const std::string& bottom, const std::string& left, const std::string& right, const std::string& front, const std::string& back) {
    if (Renderer::GetAPI() == RendererAPI::API::OpenGL) {
        Ref<CubeMap> result = CubeMap::Create();
        result->SetData(CubeMap::FRONT, front);
        result->SetData(CubeMap::BACK, back);
        result->SetData(CubeMap::LEFT, left);
        result->SetData(CubeMap::RIGHT, right);
        result->SetData(CubeMap::TOP, top);
        result->SetData(CubeMap::BOTTOM, bottom);
        return result; 
    }
    XEN_CORE_ASSERT(false, "Unknown API type");
    return nullptr;
}

static void InitSkyBoxData() {
    float skyboxVertices[] = {
        // positions          
        -1.0f,  1.0f, -1.0f,
        -1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, -1.0f,
        1.0f,  1.0f, -1.0f,
        -1.0f,  1.0f, -1.0f,

        -1.0f, -1.0f,  1.0f,
        -1.0f, -1.0f, -1.0f,
        -1.0f,  1.0f, -1.0f,
        -1.0f,  1.0f, -1.0f,
        -1.0f,  1.0f,  1.0f,
        -1.0f, -1.0f,  1.0f,

        1.0f, -1.0f, -1.0f,
        1.0f, -1.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        1.0f,  1.0f, -1.0f,
        1.0f, -1.0f, -1.0f,

        -1.0f, -1.0f,  1.0f,
        -1.0f,  1.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        1.0f, -1.0f,  1.0f,
        -1.0f, -1.0f,  1.0f,

        -1.0f,  1.0f, -1.0f,
        1.0f,  1.0f, -1.0f,
        1.0f,  1.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        -1.0f,  1.0f,  1.0f,
        -1.0f,  1.0f, -1.0f,

        -1.0f, -1.0f, -1.0f,
        -1.0f, -1.0f,  1.0f,
        1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, -1.0f,
        -1.0f, -1.0f,  1.0f,
        1.0f, -1.0f,  1.0f
    };

    auto vertex_buffer = VertexBuffer::Create(skyboxVertices, sizeof(skyboxVertices));
    BufferLayout layout = {
        {"aPos", ShaderDataType::Float3}
    };
    vertex_buffer->SetLayout(layout);
    data.skybox_vao = VertexArray::Create();
    data.skybox_vao->SetVertexBuffer(vertex_buffer);
}

void Renderer3D::Init() {
    uint32_t white_color = 0xFFFFFFFF;
    data.white_texture = Texture2D::CreateColorTexture(1, 1);
    data.white_texture->SetData(&white_color);

    uint32_t black_color = 0xFF000000;
    data.black_texture = Texture2D::CreateColorTexture(1, 1);
    data.black_texture->SetData(&black_color);

    uint32_t blue_color = 0xFFFF0000;
    data.blue_texture = Texture2D::CreateColorTexture(1, 1);
    data.blue_texture->SetData(&blue_color);

    data.shader = Shader::CreateFromFile("Texture3D", PROJECT_PATH "/sandbox/assets/shaders/Texture3D.glsl");

    data.skybox_shader = Shader::CreateFromFile("SkyBox", PROJECT_PATH "/sandbox/assets/shaders/SkyBox.glsl");
    InitSkyBoxData();

    RenderCommand::EnableDepthTest();
}

void Renderer3D::EnableFaceCull() {
    RenderCommand::EnableCullFace();
}

void Renderer3D::DisableFaceCull() {
    RenderCommand::DisableCullFace();
}

void Renderer3D::Shutdown() {}

Ref<Material> Renderer3D::CreateMaterial() {
    return std::make_shared<Material>(data.shader);
}

void Renderer3D::SetSkyBox(const Ref<CubeMap>& skybox) {
    data.skybox = skybox;
}

Ref<CubeMap> Renderer3D::GetSkyBox() {
    return data.skybox;
}

void Renderer3D::DrawSkyBox() {
    if (data.skybox) {
        data.skybox_shader->Bind();
        data.skybox->Bind();

        bool depth_test_enabled = IsEnabelDepthTest();

        EnableDepthTest();
        SetDepthFunc(DepthFunc::LessEqual);

        data.skybox_shader->Bind();
        data.skybox_shader->UniformMat4("projection", data.projection);
        glm::mat4 view = glm::mat4(glm::mat3(data.view));
        data.skybox_shader->UniformMat4("view", view);
        data.skybox_vao->Bind();
        RenderCommand::DrawArrays(data.skybox_vao, 0, 36);

        SetDepthFunc(DepthFunc::Less);
        if (!depth_test_enabled) {
            DisableDepthTest();
        }
    }

}

void Renderer3D::BeginScene(const PerspectiveCamera& camera, const DirectLight& dirlight, const DotLight& dotlight, const SpotLight& spotlight) {
    data.dirlight = dirlight;
    data.dotlight = dotlight;
    data.spotlight = spotlight;
    data.view_pos = camera.GetPosition();
    data.view = camera.GetViewMat();
    data.projection = camera.GetProjection();
    Renderer::BeginScene(camera);
}

void Renderer3D::EndScene() {
    data.shader->Unbind();
    Renderer::EndScene();
}

glm::mat4 CalculateModelMat(const glm::vec3& center, const glm::vec3& rotation, const glm::vec3& scale) {
    glm::mat4 model = glm::translate(glm::mat4(1.0f), center)*
                        glm::scale(glm::mat4(1.0f), scale) *
                        glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) *
                        glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) *
                        glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1));
    return model;
}

void Renderer3D::DrawMesh(const Ref<Mesh>& mesh, const glm::vec3& center, const glm::vec3& rotation, const glm::vec3& scale) {
    data.shader->Bind();

    // uniform dirlight
    data.shader->UniformFloat3("dirlight.phong.ambient", data.dirlight.phong.ambient);
    data.shader->UniformFloat3("dirlight.phong.diffuse", data.dirlight.phong.diffuse);
    data.shader->UniformFloat3("dirlight.phong.specular", data.dirlight.phong.specular);
    data.shader->UniformFloat3("dirlight.direction", data.dirlight.direction);

    // uniform dotlight
    data.shader->UniformFloat3("dotlight.phong.ambient", data.dotlight.phong.ambient);
    data.shader->UniformFloat3("dotlight.phong.diffuse", data.dotlight.phong.diffuse);
    data.shader->UniformFloat3("dotlight.phong.specular", data.dotlight.phong.specular);
    data.shader->UniformFloat3("dotlight.position", data.dotlight.position);
    data.shader->UniformFloat("dotlight.constant", data.dotlight.attenuation.constant);
    data.shader->UniformFloat("dotlight.linear", data.dotlight.attenuation.linear);
    data.shader->UniformFloat("dotlight.quadratic", data.dotlight.attenuation.quadratic);

    // uniform spotlight
    data.shader->UniformFloat3("spotlight.phong.ambient", data.spotlight.phong.ambient);
    data.shader->UniformFloat3("spotlight.phong.diffuse", data.spotlight.phong.diffuse);
    data.shader->UniformFloat3("spotlight.phong.specular", data.spotlight.phong.specular);
    data.shader->UniformFloat3("spotlight.position", data.spotlight.position);
    data.shader->UniformFloat3("spotlight.direction", data.spotlight.direction);
    data.shader->UniformFloat("spotlight.inner_cutoff", cos(glm::radians(data.spotlight.cutoff.innerCutOff)));
    data.shader->UniformFloat("spotlight.outer_cutoff", cos(glm::radians(data.spotlight.cutoff.outerCutOff)));

    // view_pos
    data.shader->UniformFloat3("view_pos", data.view_pos);

    auto material = mesh->GetMaterial();
    auto idx = 0;
    if (material) {
        idx = material->GetTextureNum();
        if (!material->GetTexture("material.diffuse_texture")) {
            data.white_texture->Bind(idx);
            data.shader->UniformInt("material.diffuse_texture", idx);
            idx++;
        } else {
            data.shader->UniformFloat3("material.diffuse", glm::vec3(1, 1, 1));
        }
        if (!material->GetTexture("material.specular_texture")) {
            data.white_texture->Bind(idx);
            data.shader->UniformInt("material.specular_texture", idx);
            idx++;
        } else {
            data.shader->UniformFloat3("material.specular", glm::vec3(1, 1, 1));
        }
        if (!material->GetTexture("material.emissive_texture")) {
            data.white_texture->Bind(idx);
            data.shader->UniformInt("material.emissive_texture", idx);
            idx++;
        } else {
            data.shader->UniformFloat3("material.emission", glm::vec3(1, 1, 1));
        }
        if (!material->GetTexture("material.normal_texture")) {
            data.blue_texture->Bind(idx);
            data.shader->UniformInt("material.normal_texture", idx);
            idx++;
        }
        if (material->GetData("material.shininess").type == ShaderDataType::None) {
            data.shader->UniformFloat("material.shininess", 0);
        }
        if (material->GetData("color").type == ShaderDataType::None) {
            data.shader->UniformFloat4("color", {1, 1, 1, 1});
        }
        material->Use();
    }

    glm::mat4 model = CalculateModelMat(center, rotation, scale);

    Renderer::Submit(mesh->vertex_array_, data.shader, model);
}

void Renderer3D::DrawModel(const Ref<Model>& model, const glm::vec3& center, const glm::vec3& rotation, const glm::vec3& scale) {
    for (auto& mesh : model->GetMeshes()) {
        Renderer3D::DrawMesh(mesh, center, rotation, scale);
    }
}

}
